package org.unidata.mdm.rest.v1.meta.service.model;

import static org.unidata.mdm.meta.type.rendering.MetaModelInputRenderingAction.APPLY_META_MODEL_GRAPH;

import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.context.ModelChangeContext.ModelChangeType;
import org.unidata.mdm.core.type.model.StorageElement;
import org.unidata.mdm.core.type.model.StorageInstance;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.context.ApplyUploadedModelRequestContext;
import org.unidata.mdm.meta.context.GetDataModelContext;
import org.unidata.mdm.meta.context.UpsertDataModelContext;
import org.unidata.mdm.meta.service.MetaDependencyService;
import org.unidata.mdm.meta.type.input.meta.MetaGraph;
import org.unidata.mdm.meta.type.input.meta.MetaType;
import org.unidata.mdm.meta.type.instance.DataModelInstance;
import org.unidata.mdm.meta.type.model.DataModel;
import org.unidata.mdm.rest.system.ro.DetailedErrorResponseRO;
import org.unidata.mdm.rest.v1.meta.converter.GenericModelInfoConverter;
import org.unidata.mdm.rest.v1.meta.converter.graph.MetaGraphDTOToROConverter;
import org.unidata.mdm.rest.v1.meta.converter.graph.MetaGraphROToDTOConverter;
import org.unidata.mdm.rest.v1.meta.converter.graph.MetaTypeROToDTOConverter;
import org.unidata.mdm.rest.v1.meta.ro.GetGenericModelInfoResultRO;
import org.unidata.mdm.rest.v1.meta.ro.UpsertGenericModelInfoRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.model.ApplyGraphRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.model.ApplyGraphResultRO;
import org.unidata.mdm.rest.v1.meta.ro.model.GetMetaDependencyRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.model.GetMetaDependencyResultRO;
import org.unidata.mdm.rest.v1.meta.ro.model.GetStorageIdsResultRO;
import org.unidata.mdm.rest.v1.meta.ro.model.ImportDataModelResultRO;
import org.unidata.mdm.rest.v1.meta.service.AbstractMetaModelRestService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * Meta model rest controller
 *
 * @author Alexandr Serov
 * @since 27.11.2020
 **/
@Path(MetaModelRestService.SERVICE_PATH)
@Consumes({"application/json"})
@Produces({"application/json"})
public class MetaModelRestService extends AbstractMetaModelRestService {

    /**
     * Service path.
     */
    static final String SERVICE_PATH = "model";

    static final String SERVICE_TAG = "model";

    /**
     * Meta dependency service.
     */
    @Autowired
    private MetaDependencyService metaDependencyService;


    /**
     *
     */
    @GET
    @Path("/storageIds")
    @Operation(
        description = "List of model storage ids.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG
    )
    public GetStorageIdsResultRO storageIds() {
        GetStorageIdsResultRO result = new GetStorageIdsResultRO();
        StorageInstance storageInstance = metaModelService.getStorageInstance();
        Collection<StorageElement> storages = ObjectUtils.defaultIfNull(storageInstance.getActive(), Collections.emptyList());
        result.setStorageIds(storages.stream()
            .map(StorageElement::getStorageId)
            .collect(Collectors.toList())
        );
        return result;
    }

    @POST
    @Path("/info")
    @Operation(
        description = "Upsert generic source systems model info.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = UpsertGenericModelInfoRequestRO.class)),
            description = "Upsert request."),
        tags = SERVICE_TAG)
    public GetGenericModelInfoResultRO upsert(UpsertGenericModelInfoRequestRO req) {

        metaModelService.upsert(UpsertDataModelContext.builder()
                .name(req.getName())
                .displayName(req.getDisplayName())
                .description(req.getDescription())
                .draftId(req.getDraftId())
                .parentDraftId(req.getParentDraftId())
                .build());

        return GenericModelInfoConverter.to(metaModelService.get(GetDataModelContext.builder()
                .modelInfo(true)
                .draftId(req.getDraftId())
                .build()));
    }

    @GET
    @Path("/info")
    @Operation(
        description = "Gets generic source systems model info.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetGenericModelInfoResultRO get(
            @Parameter(description = "Draft ID. Optional.", in = ParameterIn.QUERY)
            @QueryParam("draftId") @DefaultValue("0") Long draftId) {
        return GenericModelInfoConverter.to(metaModelService.get(GetDataModelContext.builder()
                .draftId(draftId)
                .modelInfo(true)
                .build()));
    }

    /**
     * Gets model data associated with optional storage ID. Returns current, if no
     * storage id specified.
     *
     * @return character stream
     */
    @GET
    @Path("/export")
    @Produces(MediaType.TEXT_XML)
    @Operation(
        description = "Gets the data model XML by optional storage ID.",
        method = HttpMethod.GET,
        parameters = {@Parameter(description = "Storage ID. Optional", in = ParameterIn.QUERY, name = "storageId")},
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(type = "string", format = "binary")), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
        },
        tags = SERVICE_TAG
    )
    public Response exportDataModel(@QueryParam("storageId") String storageId) {
        DataModelInstance model = instance(Descriptors.DATA, storageId);
        return exportXmlFile(model.getStorageId(), model.toSource());
    }

    /**
     * Saves binary large object.
     *
     * @param recreate the recreate
     * @param attachment attachment object
     */
    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Path("/import")
    @Operation(
        description = "Upload data model (partial or full).",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA), description = "Request"),
        tags = SERVICE_TAG
    )
    public ImportDataModelResultRO importDataModel(
        @Multipart(value = "recreate") Boolean recreate,
        @Multipart(value = "file") Attachment attachment) {
        ModelChangeType type = BooleanUtils.isTrue(recreate) ? ModelChangeType.FULL : ModelChangeType.MERGE;
        return importXmlFile(attachment, DataModel.class, model -> {
            ImportDataModelResultRO result = new ImportDataModelResultRO();
            result.setInstanceId(model.getInstanceId());
            result.setStorageId(model.getStorageId());
            metaModelService.upsert(UpsertDataModelContext.builder()
                .nestedEntitiesUpdate(model.getNestedEntities())
                .lookupEntitiesUpdate(model.getLookupEntities())
                .entitiesGroupsUpdate(model.getEntitiesGroup())
                .entitiesUpdate(model.getEntities())
                .relationsUpdate(model.getRelations())
                .storageId(model.getStorageId())
                .upsertType(type)
                .waitForFinish(true)
                .build());
            return result;
        });
    }

    /**
     * Return dependency graph.
     *
     * @param request meta elements types.
     * @return dependency graph.
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/dependency")
    @Operation(
        description = "Get dependency graph.",
        method = HttpMethod.POST,
        tags = SERVICE_TAG)
    public GetMetaDependencyResultRO dependency(GetMetaDependencyRequestRO request) {

        Set<MetaType> forTypes = MetaTypeROToDTOConverter.convert(request.getForTypes());
        Set<MetaType> skipTypes = MetaTypeROToDTOConverter.convert(request.getSkipTypes());
        String storageId = request.getStorageId();
        GetMetaDependencyResultRO result = new GetMetaDependencyResultRO();
        MetaGraph graph = metaDependencyService.calculateDependencies(storageId, forTypes, skipTypes);
        result.setModelGraph(MetaGraphDTOToROConverter.convert(graph));

        return result;
    }

    /**
     * Apply graph.
     *
     * @param req request
     * @return the response
     */
    @POST
    @Path("/apply")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @Operation(
        description = "Apply meta model graph",
        method = HttpMethod.POST,
        tags = SERVICE_TAG)
    public ApplyGraphResultRO applyGraph(ApplyGraphRequestRO req) {
        Objects.requireNonNull(req, "Request can't be null");

        MetaGraph metaGraphInput = MetaGraphROToDTOConverter.convert(notNull("metaGraph", req.getMetaGraph()));
        ApplyUploadedModelRequestContext.Builder builder = ApplyUploadedModelRequestContext.builder()
            .setMetaGraph(metaGraphInput);
        ApplyGraphResultRO result = new ApplyGraphResultRO();
        renderingService.renderInput(APPLY_META_MODEL_GRAPH, builder, metaGraphInput);

        executionService.execute(builder.build());

        return result;
    }

}
